package grpc

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/jasonlvhit/gocron"
	"github.com/nuttech/bell"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/grpc/proto"
	"gitverse.ru/synapse/kubelatte/pkg/logs"
	"gitverse.ru/synapse/kubelatte/pkg/operator/controllers"
	"gitverse.ru/synapse/kubelatte/pkg/storage"
	"google.golang.org/grpc"
	"google.golang.org/grpc/connectivity"
	"google.golang.org/grpc/credentials/insecure"
	corev1 "k8s.io/api/core/v1"
	"reflect"
	"sync/atomic"
	"time"
)

type SyncController struct {
	ReSyncPeriod   uint64
	lastSyncJob    *gocron.Job
	scheduler      *gocron.Scheduler
	IsNeedHardSync atomic.Bool
	LastTime       int64
	syncObjectCh   chan interface{}
	client         proto.ConfigSyncClient
	conn           *grpc.ClientConn
}

func (w *SyncController) init() {
	if w.ReSyncPeriod <= 0 {
		w.ReSyncPeriod = 5
	}

	w.IsNeedHardSync.Store(false)
	w.syncObjectCh = make(chan interface{})

	bell.Listen(controllers.SyncUpdateReSyncPeriodEvent, w.UpdateSyncPeriod)
	bell.Listen(controllers.SyncAddNewObjectEvent, w.AddNewObject)

	logs.Debugf("[SyncController] Init with SyncPeriod %d", w.ReSyncPeriod)

	go w.runSyncLoop()
	go w.runHardSyncCheckerJob()
}

func (w *SyncController) runHardSyncCheckerJob() {
	scheduler := gocron.NewScheduler()
	w.lastSyncJob = scheduler.Every(1).Second()
	err := w.lastSyncJob.Do(w.LastSync)
	if err != nil {
		logs.Error("[SyncController] Init failed: " + err.Error())
		return
	}

	<-scheduler.Start()
}

func (w *SyncController) WaitForServerReadiness() {
	logs.Debugf("Waiting for injector grpc connection...")
	err := w.Ping()
	for err != nil {
		err = w.Ping()
		time.Sleep(1 * time.Second)
	}
}

func (w *SyncController) Ping() (err error) {
	client, done := w.GetClient()
	if done {
		return errors.New("grpc connection is done")
	}
	_, err = client.Ping(context.Background(), &proto.EmptyMessage{})
	return err
}

func (w *SyncController) SetMustHardSync() {
	if w.IsNeedHardSync.Load() {
		return
	}

	logs.Debug("[SyncController] Sync is out of date. Need full resync.")
	w.IsNeedHardSync.Store(true)
}

func (w *SyncController) LastSync() {
	ctx := context.Background()
	client, done := w.GetClient()
	if done {
		logs.Error("Can not set GRPC connection with server")
		return
	}

	if !w.checkConnection(ctx, client) {
		return
	}

	if w.IsNeedHardSync.Load() {
		w.HardSync(ctx, client)
		return
	}

	response, err := client.LastUpdate(ctx, &proto.EmptyMessage{})
	if err != nil {
		logs.Errorf("[SyncController] Cant send message to inspector %s", err.Error())
		w.SetMustHardSync()
		return
	}

	if response.Time < w.LastTime {
		logs.Warn(fmt.Sprintf("[SyncController] Sync is out of date. Server time [%d] and client time [%d].", response.Time, w.LastTime))
		w.SetMustHardSync()
		return
	}
}

func (w *SyncController) checkConnection(ctx context.Context, cli proto.ConfigSyncClient) bool {
	_, err := cli.Ping(ctx, &proto.EmptyMessage{})
	if err != nil {
		if controllers.IsInit.IsInit {
			logs.Error("GRPC server is unavailable")
		}
		return false
	}
	return true
}

func (w *SyncController) GetClient() (client proto.ConfigSyncClient, done bool) {
	if w.client == nil || w.conn == nil || (w.conn.GetState() == connectivity.TransientFailure || w.conn.GetState() == connectivity.Shutdown) {
		if w.conn != nil {
			err := w.conn.Close()
			if err != nil {
				logs.Debugf("[SyncController] Clear conn err %s", err.Error())
			}
		}

		var opts []grpc.DialOption
		var err error
		serverAddr := "localhost:8686"
		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))

		w.conn, err = grpc.Dial(serverAddr, opts...)
		if err != nil {
			logs.Errorf("[SyncController] fail to dial: %s", err.Error())
			w.SetMustHardSync()
			return nil, true
		}

		w.client = proto.NewConfigSyncClient(w.conn)
	}

	return w.client, false
}

func (w *SyncController) Start() {
	logs.Debugf("[SyncController] Start")
	go w.init()
}

func (w *SyncController) Stop() {
	logs.Debugf("[SyncController] Stop")
	if w.conn == nil {
		return
	}

	err := w.conn.Close()
	w.scheduler.Remove(w.lastSyncJob)
	if err != nil {
		logs.Errorf("[SyncController] Failure close connection %s", err.Error())
	}
}

func (w *SyncController) UpdateSyncPeriod(message bell.Message) {
	w.ReSyncPeriod = message.Value.(uint64)
	logs.Debugf("SyncController: UpdateSyncPeriod %d", w.ReSyncPeriod)
}

func (w *SyncController) AddNewObject(message bell.Message) {
	obj := message.Value.(map[string]interface{})
	w.syncObjectCh <- obj
}

func (w *SyncController) HardSync(ctx context.Context, client proto.ConfigSyncClient) {
	var err error

	lTime := time.Now().Unix()
	triggers := storage.Storage.GetTriggers()
	for _, trigger := range triggers {
		marshal, _ := json.Marshal(trigger)
		_, err = client.Sync(ctx, &proto.UpdateConfigEvent{
			Type:   "trigger",
			Action: "update",
			Json:   string(marshal),
			Time:   lTime,
		})
		if err != nil {
			logs.Errorf("[SyncController] Failed hard sync %s", err.Error())
			return
		}
	}

	templates := storage.Storage.GetTemplates()
	for _, template := range templates {
		marshal, _ := json.Marshal(template)
		_, err = client.Sync(ctx, &proto.UpdateConfigEvent{
			Type:   "template",
			Action: "update",
			Json:   string(marshal),
			Time:   lTime,
		})
		if err != nil {
			logs.Errorf("[SyncController] Failed hard sync %s", err.Error())
			return
		}
	}

	scopes := storage.Storage.GetScopes()
	for _, scope := range scopes {
		marshal, _ := json.Marshal(scope)
		_, err = client.Sync(ctx, &proto.UpdateConfigEvent{
			Type:   "scope",
			Action: "update",
			Json:   string(marshal),
			Time:   lTime,
		})
		if err != nil {
			logs.Errorf("[SyncController] Failed hard sync %s", err.Error())
			return
		}
	}

	namespaces := storage.Storage.GetNamespaces()
	for _, ns := range namespaces {
		marshal, _ := json.Marshal(ns)
		_, err = client.Sync(ctx, &proto.UpdateConfigEvent{
			Type:   "namespace",
			Action: "update",
			Json:   string(marshal),
			Time:   lTime,
		})
		if err != nil {
			logs.Errorf("[SyncController] Failed hard sync %s", err.Error())
			return
		}
	}

	w.LastTime = lTime
	w.IsNeedHardSync.Store(false)
	if triggers != nil || scopes != nil || templates != nil {
		logs.Info("[SyncController] Hard sync done")
	}
}

func (w *SyncController) runSyncLoop() {
	for {
		select {
		case v := <-w.syncObjectCh:
			client, done := w.GetClient()
			if done {
				break
			}
			go w.SendMessage(v, client)
		}
	}
}

func (w *SyncController) SendMessage(object interface{}, client proto.ConfigSyncClient) {
	ctx := context.Background()
	lTime := time.Now().Unix()

	if w.IsNeedHardSync.Load() {
		w.HardSync(ctx, client)
		return
	}

	marshal, err := json.Marshal(object.(map[string]interface{})["obj"])
	if err != nil {
		logs.Errorf("[SyncController] Json marshal error %s", err.Error())
		return
	}

	syncObj := object.(map[string]interface{})["obj"]
	tp := ""
	switch reflect.TypeOf(syncObj) {
	case reflect.TypeOf(v1alpha1.Trigger{}):
		tp = "trigger"
		break
	case reflect.TypeOf(v1alpha1.Template{}):
		tp = "template"
		break
	case reflect.TypeOf(v1alpha1.Scope{}):
		tp = "scope"
		break
	case reflect.TypeOf(corev1.Namespace{}):
		tp = "namespace"
	default:
		logs.Error("[SyncController] Unknown type %T", reflect.TypeOf(syncObj))
		return
	}

	_, err = client.Sync(ctx, &proto.UpdateConfigEvent{
		Type:   tp,
		Action: object.(map[string]interface{})["type"].(string),
		Json:   string(marshal),
		Time:   lTime,
	})

	if err != nil {
		logs.Errorf("[SyncController] Cant send message to inspector %s", err.Error())
		w.SetMustHardSync()
		return
	}

	w.LastTime = lTime
}
